use std::fmt;

use backtrace::Backtrace;
use http::StatusCode;
use syncserver_common::{impl_fmt_display, ReportableError};
use thiserror::Error;

/// Errors common to all supported syncstorage database backends. These errors can be thought of
/// as being related more to the syncstorage application logic as opposed to a particular
/// database backend.
#[derive(Debug)]
pub struct SyncstorageDbError {
    /// Enum variant defining class of `DbError`.
    kind: SyncstorageDbErrorKind,
    /// Enum variant of status code (`http::StatusCode`) name with matching code.
    pub status: StatusCode,
    /// Captured stack trace for debugging.
    pub backtrace: Backtrace,
}

/// Enum mapping of possible error types.
/// Each variant represents a distinct error condition.
/// Note the `thiserror` automatic `Display` macro string.
#[derive(Debug, Error)]
enum SyncstorageDbErrorKind {
    /// Collection cannot be found.
    #[error("Specified collection does not exist")]
    CollectionNotFound,

    /// BSO object cannot be found.
    #[error("Specified bso does not exist")]
    BsoNotFound,

    /// Specified batch could not be found.
    #[error("Specified batch does not exist")]
    BatchNotFound,

    /// Conflicting write attempt.
    #[error("An attempt at a conflicting write")]
    Conflict,

    /// Holds a message payload for arbitrary internal failures.
    #[error("Unexpected error: {}", _0)]
    Internal(String),

    /// User over quota.
    #[error("User over quota")]
    Quota,
}

/// Trait implementation that converts the provided `SyncstorageDbErrorKind` enum variant into a full
/// `SyncstorageDbError` struct. Fills in the HTTP status and a new backtrace automatically.
/// See `impl From<SyncstorageDbErrorKind> for SyncstorageDbError` below.
impl SyncstorageDbError {
    pub fn batch_not_found() -> Self {
        SyncstorageDbErrorKind::BatchNotFound.into()
    }

    pub fn bso_not_found() -> Self {
        SyncstorageDbErrorKind::BsoNotFound.into()
    }

    pub fn collection_not_found() -> Self {
        SyncstorageDbErrorKind::CollectionNotFound.into()
    }

    pub fn conflict() -> Self {
        SyncstorageDbErrorKind::Conflict.into()
    }

    pub fn internal(msg: String) -> Self {
        SyncstorageDbErrorKind::Internal(msg).into()
    }

    pub fn quota() -> Self {
        SyncstorageDbErrorKind::Quota.into()
    }
}

/// Trait that returns boolean as to whether the
/// `SyncstorageDbErrorKind` matches the kind passed in.
pub trait DbErrorIntrospect {
    fn is_collection_not_found(&self) -> bool;
    fn is_conflict(&self) -> bool;
    fn is_quota(&self) -> bool;
    fn is_bso_not_found(&self) -> bool;
    fn is_batch_not_found(&self) -> bool;
}

/// Trait implementation to match the error kind to the
/// `SyncstorageDbErrorKind` enum.
impl DbErrorIntrospect for SyncstorageDbError {
    fn is_collection_not_found(&self) -> bool {
        matches!(self.kind, SyncstorageDbErrorKind::CollectionNotFound)
    }

    fn is_conflict(&self) -> bool {
        matches!(self.kind, SyncstorageDbErrorKind::Conflict)
    }

    fn is_quota(&self) -> bool {
        matches!(self.kind, SyncstorageDbErrorKind::Quota)
    }

    fn is_bso_not_found(&self) -> bool {
        matches!(self.kind, SyncstorageDbErrorKind::BsoNotFound)
    }

    fn is_batch_not_found(&self) -> bool {
        matches!(self.kind, SyncstorageDbErrorKind::BatchNotFound)
    }
}

/// This trait comes from `syncserver_common`.
/// It defines how errors should be reported (to logs, metrics, or Sentry).
impl ReportableError for SyncstorageDbError {
    fn reportable_source(&self) -> Option<&(dyn ReportableError + 'static)> {
        None
    }

    fn is_sentry_event(&self) -> bool {
        !matches!(&self.kind, SyncstorageDbErrorKind::Conflict)
    }

    fn metric_label(&self) -> Option<&'static str> {
        match self.kind {
            SyncstorageDbErrorKind::Conflict => Some("storage.conflict"),
            _ => None,
        }
    }

    fn backtrace(&self) -> Option<&Backtrace> {
        Some(&self.backtrace)
    }
}

impl From<SyncstorageDbErrorKind> for SyncstorageDbError {
    /// Converts the provided `SyncstorageDbErrorKind` enum into a full SyncstorageDbError struct.
    /// Fills in the HTTP status and a new backtrace automatically.
    fn from(kind: SyncstorageDbErrorKind) -> Self {
        let status = match kind {
            SyncstorageDbErrorKind::CollectionNotFound | SyncstorageDbErrorKind::BsoNotFound => {
                StatusCode::NOT_FOUND
            }
            // Matching the Python code here (a 400 vs 404)
            SyncstorageDbErrorKind::BatchNotFound => StatusCode::BAD_REQUEST,
            // NOTE: the protocol specification states that we should return a
            // "409 Conflict" response here, but clients currently do not
            // handle these respones very well:
            //  * desktop bug: https://bugzilla.mozilla.org/show_bug.cgi?id=959034
            //  * android bug: https://bugzilla.mozilla.org/show_bug.cgi?id=959032
            SyncstorageDbErrorKind::Conflict => StatusCode::SERVICE_UNAVAILABLE,
            SyncstorageDbErrorKind::Quota => StatusCode::FORBIDDEN,
            _ => StatusCode::INTERNAL_SERVER_ERROR,
        };

        Self {
            kind,
            status,
            backtrace: Backtrace::new(),
        }
    }
}

impl_fmt_display!(SyncstorageDbError, SyncstorageDbErrorKind);
